Angular имеет отличный инструмент для ограничения навигации между страницам из коробки. Но, как и любом большом проекте, у него есть свои подводные камни. Сегодня я расскажу вам об одном из них.
Перед прочтением статьи освежите знания по Angular Guards (далее гарды). Статья будет о CanActivate
, но это относиться так же и к остальным гардам. Теперь перейдем к истории обнаружения приоритетов.
Первая проба
Недавно я разрабатывал новую фичу для админов. Требовалось ограничить доступ на страницу — пускать пользователя, если у него включен определенный фича флаг и он администратор, в противном случае редирект на главную.
Кажется, что задача тривиальная. В моем мозгу сразу появилось решение с помощью гарда CanActivate
. Допустим, уже существуют два сервиса на эти два условия. Далее вызываем нужные методы, объединяем их в один Observable
и дальше если хоть один вернул false
, то редиректим на главную, если оба true
, то пускаем пользователя к странице.
@Injectable() export class AdminGuard implements CanActivate { constructor( private readonly featureToggleService: FeatureToggleService, private readonly userService: UserService, private readonly router: Router ) {} canActivate(): Observable<boolean | UrlTree> { const isAdminPageEnabled$ = this.featureToggleService.isAdminPageEnabled$.pipe(startWith(null)); const isAdmin$ = this.userService.isAdmin$.pipe(startWith(null)); return combineLatest([isAdminPageEnabled$, isAdmin$]).pipe( first( conditions => conditions.some(condition => condition === false) || conditions.every(Boolean) ), map( ([isAdminPageEnabled, isAdmin]) => (isAdminPageEnabled && isAdmin) || this.router.createUrlTree(['/']) ) ); } }
Код работает, но если присмотреться, то можно написать лучше. В реализации слишком много ответственности на один маленький гард, и код не читаемый. Поправим это.
Рефакторинг
Раз слишком много ответственности, можно разделить гарды на два, учитывая, что свойство CanActivate
у роута принимает массив гардов.
// admin-page-feature.guard.ts @Injectable() export class AdminPageFeatureGuard implements CanActivate { constructor( private readonly featureToggleService: FeatureToggleService, private readonly router: Router ) {} canActivate(): Observable<boolean | UrlTree> { return this.featureToggleService.isAdminPageEnabled$.pipe( map( isAdminPageEnabled => isAdminPageEnabled || this.router.createUrlTree(['/']) ) ); } } // admin.guard.ts @Injectable() export class AdminGuard implements CanActivate { constructor( private readonly userService: UserService, private readonly router: Router ) {} canActivate(): Observable<boolean | UrlTree> { return this.userService.isAdmin$.pipe( map(isAdmin => isAdmin || this.router.createUrlTree(['/'])) ); } }
Теперь стало два гарда, которые можно переиспользовать, и код стал читаемый. Казалось бы все отлично, но на этом моменте я озадачился одним вопросом. Гарды в массиве CanActivate
выполняются последовательно или параллельно?
const routes: Routes = [ { path: 'admin', component: AdminPageComponent, canActivate: [AdminGuard, AdminPageFeatureGuard], }, ];
Дела мои плохи, если гарды выполняются последовательно, так как нет смысла заставлять пользователя ждать каждый гард отдельно.
Я провел исследование и выяснил, что они выполняются параллельно, но с некоторыми нюансами.
Приоритеты гардов
В далекой седьмой версии Angular завезли приоритет для гардов. Переданные в массив для CanActivate
гарды будут выполняться параллельно, но роутер будет ждать, пока более высокий приоритет завершится, чтобы двигаться дальше. Приоритет определяется порядком в массиве СanActivate
.
Разберемся на примере. Есть два гарда HighPriorityGuard
и LowPriorityGuard
. Оба возвращают Observable<boolean | UrlTree>. Мы производим навигацию по роуту, где применяются эти гарды.
canActivate: [HighPriorityGuard, LowPriorityGuard]
Факты о выполнение гардов:
-
Запускаются параллельно.
-
Если
LowPriorityGuard
возвращаетUrlTree|false
первым, то роутер все равно будет дожидаться выполненияHighPriorityGuard
перед тем, как начать навигацию. -
Если
HighPriorityGuard
возвращает первымUrlTree
, то сразу будет произведена навигация наUrlTree
изHighPriorityGuard
. -
Если
LowPriorityGuard
возвращаетUrlTree
первым, а далееHighPriorityGuard
возвращаетUrlTree
, то навигация произойдет наUrlTree
изHighPriorityGuard
, так как побеждает более высокий приоритет. -
Если
HighPriorityGuard
возвращаетtrue
первым, роутер будет ждатьLowPriorityGuard
, так как не может производить навигацию, пока не убедится, что все гарды возвращаютtrue
.
Если остались вопросы по приоритетам гардов, пожалуйста, перейдите по этой ссылке, я подготовил небольшое демо, там будет проще разобраться.
Зачем нужны приоритеты
Команда Angular добавила приоритет для гардов, чтобы решить проблему множественных редиректов. Допустим, в приложение есть два гарда — аутентификации и админа. Гард аутентификации, будет отправлять на страницу логина, если пользователь не залогинился. А гард админа, будет отправлять на страницу “Вы не админ”, если у пользователя нет нужной роли. До седьмой версии Angular, навигация выполнялась из гарда, который первый сделает редирект. И выходило так что пользователь, который не залогинен, может попасть на страницу “Вы не админ”, потому что гард админа выполнился быстрее.
Но теперь мы имеем UrlTree
и приоритет гардов. Если гард аутентификации важнее, то ставим его первым в массив, тем самым даем ему более высокий приоритет, тогда роутер будет ждать его выполнения и проводить редирект на UrlTree
из него.
Краткие итоги
Наделяйте гарды только одной ответственностью, тогда их легче читать и переиспользовать. Но помните про ловушку приоритетов. Если у вас первый гард выполняется за секунду, а второй за час, и нет разницы по редиректу, то стоит подумать о порядке в массиве CanActivate/CanActivateChild/CanLoad/CanDeactivate
, чтобы иногда пользователь не ждал один час.
Я написал свое решение для обхода приоритета гардов, потому что меня категорично не устраивало заставлять пользователя ждать, но об этом в следующей статьей.
ссылка на оригинал статьи https://habr.com/ru/post/689682/
Добавить комментарий